Repository navigation
fix(objectql): multi update 的 SET 载荷剥掉非 id 的 data.id (#6262) - #6433
Conversation
…payload (#6262) `update(o, { id: { $in: ['a','b'] }, title: 'x' }, { multi: true })` has dispatched correctly since #5748 / PR #5919 — an operator object is not a primary key, so it stops shadowing the ladder and the declared bulk intent is honoured (`driver.updateMany`). What that fix did not do is clean the PAYLOAD. Measured on origin/main with a recording driver over the real engine: updateMany({ object: 'probe_task' }, { id: { $in: ['a','b'] }, title: 'x' }) i.e. the driver is asked to write a serialized operator object into the primary-key column of every matched row. Five backends would each answer that differently (the #5240 / #4434 family), and on the ones that accept it the matched rows lose their identity irreversibly. Reaching the multi branch AT ALL means `resolveEngineUpdateDispatch` returned `multi`, i.e. it found no scalar truthy id in EITHER source — so whatever sits in `data.id` there is a value the engine has already RULED is not a primary key. The strip is that same answer applied one layer on, not a second opinion: a value that is not the primary key does not get to sit in the primary-key column either. - Zero verdict change: `ENGINE_UPDATE_DISPATCH_CASES` is untouched and `operator object in data.id WITH multi:true` still expects 'multi'. Rejecting the call instead (#6262 route B) would reverse that just-landed case — a partial rollback of #5748's ruling A, which needs a fresh decision. - No reachable legitimate write is lost: a truthy scalar `data.id` outranks both `where` and `multi` and never reaches this branch, and N rows cannot share one primary key anyway. - The by-id path is unchanged and pinned as-is: `driver.update` takes the primary key in its own argument, so the key in the payload is redundant rather than damaging. - Falsy scalars keep the #5747 / #5748 dispatch semantics (still 'multi') and are stripped on the same argument — stripping operator objects while leaving `{ id: 0 }` in would be a second rule about one fact. The drop logs at warn, naming the consequence and both correct spellings. Deliberately not routed through `onFieldsDropped`: `DroppedFieldsEvent.reason` is a closed enum over the two read-only strips (#3407 / #3042), and widening that vocabulary is a `packages/spec` change with its own consumers. Fixes #6262 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…ti-payload-id-strip
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
范围外发现(PD #10,均未在本 PR 内修改)
Generated by Claude Code |
…e payload (objectstack-ai#6435) (objectstack-ai#6475) The by-id half of objectstack-ai#6262 / PR objectstack-ai#6433. When `data.id` is a non-scalar (operator object, array, `null`) or a falsy scalar and `options.where.id` is a truthy scalar, `resolveEngineUpdateDispatch` correctly rules the payload value is not a primary key and binds `where.id` instead (objectstack-ai#5748 / PR objectstack-ai#5919). The dispatch was right; the PAYLOAD was never cleaned, so `driver.update(object, 'rec_1', data)` carried the ruled-not-an-id value into the SET clause and driver-sql wrote `UPDATE task SET id = '{"$in":["a","b"]}' WHERE id = 'rec_1'` — the row's identity overwritten irreversibly. Route A only: strip that payload `id`, on a copy, leaving a truthy scalar `data.id` exactly as it was (there the payload key IS the bound id — a same-value no-op). Zero dispatch verdicts change; membership is asked by calling the producer's own `resolveEngineUpdateDispatch`, never by re-deriving the unexported scalar test. Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We Co-authored-by: Claude <noreply@anthropic.com>
…ed id strip (objectstack-ai#6437) (objectstack-ai#7125) `DroppedFieldsEvent.reason` was a closed enum over the two READ-ONLY strips, so the primary-key strip added by objectstack-ai#6262 / PR objectstack-ai#6433 (multi) and objectstack-ai#6435 (by-id) was invisible to `onFieldsDropped` and `strictReadonlyWrites`. Adds `primary_key` and routes both strip sites through `reportDroppedFields`. Because `strictReadonlyWrites` coverage is DERIVED from the reported set (measured: `strictDrops.push` applies no reason-class filter), reporting the new reason also adds a refusal — deliberate, documented, pinned both ways. The refusal message is now composed from `drops` so a `primary_key` rejection never claims the field was read-only; the read-only-only message stays byte-identical. Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2 Co-authored-by: Claude <noreply@anthropic.com>
… commits that decided them (stage 4) (objectstack-ai#20548) Part of objectstack-ai#20234 Clause-②: no ## What changed This is stage 4 of the staged sweep: the `data/` remainder. It covers the six `packages/spec/src/data/` files stage 3 (PR objectstack-ai#20533, landed `03b19d9cfd`) left out because an open PR held them, and nothing else. They are `object.zod.ts`, `filter-logic-conformance.ts`, `object.form.ts`, `data-engine.zod.ts`, `data-engine.test.ts` and `hook.form.ts`. Later stages cover the other areas, so this PR says `Part of`. The census below measured all six. Three of them carry comment or docblock sites that cite a tracker number answering 404. `data-engine.zod.ts`, `data-engine.test.ts` and `hook.form.ts` carry none, so they are not in the diff. Every such site has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123). That is **19 sites on 19 lines in 3 files, covering 9 numbers**. Each rewritten line now cites the commit in `origin/main` history that decided what the line describes, and it says in its own words what that commit decided. Where a PR number was already on the line (`PR objectstack-ai#13529`), it stays beside the commit as the link. No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of the 9 numbers: a search for each number, with and without `#`, finds nothing there. So every anchor is a commit: **9 distinct shas**. Stage 3 had already read these commits and recorded them in PR objectstack-ai#20533's body. They were not copied from there. Each one was re-read against the current line it anchors: its own message or diff names the number it replaces, and it made the change the line describes. `object.zod.ts` and `filter-logic-conformance.ts` moved on `main` after stage 3 read them (PRs objectstack-ai#20521 and objectstack-ai#20523). Each site was therefore re-read at this base, `03b19d9cfd`. Only comments changed. Every source file keeps its line count (20 lines out, 20 in, over 3 files), so no line citation into these files moves. One of the 20 lines held no dead citation: `filter-logic-conformance.ts:249`, the first half of a sentence reflowed onto `:250`. No code token moves (see the guard below). **No tracker number is added.** Every tracker number on an added line was already in the hunk it replaces. `PR objectstack-ai#13529` stands on three added lines, and on the three removed lines of the same hunks. It is the link beside commit `9dac1ae01`, which stage 3 recorded the same way. No reference page under `content/docs/references/` moved: none of the rewritten docblocks projects into one (`check:docs` at the head: `226 generated files in sync`). The PR adds one `patch` changeset for `@objectstack/spec` (see Changeset below). ## Census: the six files, before and after **Instrument.** This is the instrument of stages 1 to 3. It sends REST `GET /repos/objectstack-ai/objectstack/issues/N` without following redirects, for every distinct number cited in `packages/spec/src/data`. The population is: - the citation gate's own exported `CITATION_RE` and `NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`, `objectstack-ai/objectstack`, `framework`, `pre-` or `post-`; - widened case-insensitively to `Pre-`, `POST-` and `Framework`, as in stage 3; - N of 100 or more, excluding `summon` heads. Each site is classified by the TypeScript parser as a line comment, a docblock, a block comment or a string. Two cross-checks close the population. First, a raw `#N` count in each of the six files equals the census rows plus the cross-repo rows in five files. In the other two it is one higher, and the extra is a second number after a slash inside a string (`objectstack-ai#5322/objectstack-ai#5134` in a `note`, `objectstack-ai#6262/objectstack-ai#6433` in a test title). Both answer 200. Second, no spelled citation (`issue N`, `PR N`, `card N`) occurs in any of the six. **Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at the start, after every 100 numbers and at the end: 24 of 24 lit (200) and 24 of 24 dead (404) over 8 checkpoints in the base run, and 21 of 21 lit and 21 of 21 dead over 7 checkpoints in the head run. | reading | tree | numbers probed | 200 | 404 | 301 or other | dead sites, all of `data/` | dead sites, the six files | lines | files | numbers | |---|---|---|---|---|---|---|---|---|---|---| | before | base `03b19d9cfd`, probed 2026-09-29T01:11:59Z to 01:15:49Z | 601 | 572 | 29 | 0 | **77** | 19 | 19 | 3 | 9 | | after | head `53c9070dfd`, probed 2026-09-29T01:25:55Z to 01:29:35Z | 597 | 572 | 25 | 0 | **58** | 0 | 0 | 0 | 0 | The head probe found no number newly dead since the base probe: the same 572 numbers answer 200. The base reading of 77 equals stage 3's after reading at `96fd49caa2`. **Per file.** Cited sites here are every in-repo citation the population reads, live or dead. | file | cited sites (base) | dead sites before | by class | dead sites after | |---|---|---|---|---| | `object.zod.ts` | 120 | 15 | 8 docblock, 7 line comment | 0 | | `filter-logic-conformance.ts` | 97 | 3 | 2 docblock, 1 line comment | 0 | | `object.form.ts` | 31 | 1 | 1 line comment | 0 | | `data-engine.zod.ts` | 48 | 0 | | 0 | | `data-engine.test.ts` | 29 | 0 | | 0 | | `hook.form.ts` | 0 | 0 | | 0 | None of the 19 sites is a string, so this stage leaves no string token behind. ## Per-number table | number | sites / lines | anchor: what it decided | |---|---|---| | `objectstack-ai#8772` | 4 / 4, `object.zod.ts:2718`, `:2731`, `:2744`, `:2910` | `75b7c240a`: Direction 2 of the 2026-08-16 maintainer ruling. `ObjectSchema.create()` forces `required: true` on a `master_detail` reference under `controlled_by_parent` and refuses an explicit `required: false`. Raw parse stays tolerant, and runtime tolerance is the ruling's other half. Its changeset records the measurement that only the security gate closed that shape while the declaration surface accepted it (`:2731`). ADR-0055 stays cited beside it. It is the same anchor stage 3 gave `object.test.ts` | | `objectstack-ai#10165` | 2 / 2, `object.zod.ts:818`, `:1036` | `801296050`: `ttl.onlyWhen` with the canonical null predicate (maintainer ruling 2026-08-20, option A). One shared `onlyWhen` union, and both of `retention.onlyWhen`'s conflicts mirrored. Its diff wrote both `[objectstack-ai#10165]` blocks | | `objectstack-ai#10347` | 3 / 3, `object.zod.ts:1006`, `:1042`, `:1049` | `530c1df65`: the Archiver honours a declared `ttl`. It selects by the ttl cutoff on `ttl.field` when `ttl` is declared, and by `created_at` / `archive.after` otherwise (maintainer ruling 2026-08-20) | | `objectstack-ai#10527` | 1 / 1, `object.zod.ts:1005` | `5649efbf9`: refuses a diverging retention + ttl + archive triple at parse time. Its diff wrote this very paragraph | | `objectstack-ai#11195` | 1 / 1, `object.zod.ts:1791` | `b37231883`: `UserActionsConfigSchema` adopts `group` / `hideFields` / `rowColor` (the "last three" the line names) | | `objectstack-ai#11408` | 1 / 1, `object.zod.ts:2189` | `f11fc61c5`: declares `editMode` on the object document (maintainer ruling 2026-08-24, the `objectstack-ai#10144` declare-or-rule-out family, which stays cited) | | `objectstack-ai#13608` | 3 / 3, `object.zod.ts:2317`, `:2354`, `:2366` | `fc9ba76a5`: `publicSharing.eligibility` is held at redemption, not only at mint, fail-closed, with the undifferentiated `null` refusal. Its changeset heads with objectstack-ai#13608. It is the same anchor stage 1 gave `contracts/share-link-service.ts` | | `objectstack-ai#13195` | 3 / 3, `filter-logic-conformance.ts:190`, `:250`, `:525` | `9dac1ae01`, PR objectstack-ai#13529's squash commit, which stays as the link: `$exists` means has-a-value on driver-memory's live mingo path, its analytics face and driver-mongodb's `translateFilter` (the "last three key-presence exits") | | `objectstack-ai#12868` | 1 / 1, `object.form.ts:256` | `c459da6bc`: narrows the per-option `default` key out of the form-view options vocabulary, which offered a key nothing on that surface read. Commit `e808890958`, which wrote this line, names objectstack-ai#12868 as the same offer-vs-door class | The shas were checked at the base and again at `origin/main` `288611e3e5`. Every one matches exactly one commit (`git rev-parse --disambiguate`, count 1). Every one is an ancestor (`git merge-base --is-ancestor`, exit 0 for 9 of 9). The control leg `e9584681a4` also exits 0, and the repository is not shallow. For each commit, a grep of its own message or diff finds the number it replaces. Seven of the nine name it in the message. `fc9ba76a5` names it in its diff (20 lines, including its changeset heading), and so does `c459da6bc` (8 lines, including its changeset heading). Wordings to check, each true of its commit: - `object.zod.ts:2731` now reads 「closes that shape, and commit 75b7c24 records that the declaration and the enforcement disagree」. The measurement was the card's. The commit's changeset records it: "only the security gate closed that shape while the declaration surface accepted it". - `object.zod.ts:2189` reads 「Declared here by commit f11fc61's maintainer ruling」, and `:2744` reads 「the other half of commit 75b7c24's ruling」. This is stage 3's wording for the same relation (`object.test.ts`, 「the other half of commit 75b7c24's ruling」): the commit that landed the ruling and quotes it. - `object.zod.ts:1049` reads 「That is the whole of what [commit 530c1df] changed here」. Commit `52db1d1f2a` wrote the paragraph. `530c1df65` is the change it describes. ## Mechanical guard: no code token moves The check compares leaf tokens with comments stripped, base `03b19d9cfd` against head `53c9070dfd`. It uses the TypeScript parser's leaf tokens (TypeScript from the head's lockfile), so template literals are scanned in context, and it excludes JSDoc nodes. It ran over all 3 touched `.ts` files. It is the stage-3 instrument, unchanged. - Real run: 13,624 base tokens (object.zod.ts 8,774, object.form.ts 3,226, filter-logic-conformance.ts 1,624), **0 files with a token change** (exit 0). - Comment-insertion control (`object.form.ts`): 0 files changed, as expected (exit 0). - Positive control (a declaration inserted into `object.zod.ts`): 1 file reads DIFFER at token 1629 (exit 1). - Positive control (one digit changed inside the `objectstack-ai#5322/objectstack-ai#5134` `note` string in `filter-logic-conformance.ts`): 1 file reads DIFFER at token 889 (exit 1). Line balance: `object.zod.ts` +15 / -15, `filter-logic-conformance.ts` +4 / -4, `object.form.ts` +1 / -1. Line counts are equal at base and head: 3,240, 621 and 751. ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/spec` is included. It says only that the provenance comments were re-anchored. `Clause-②: no`: no export, key, value or type moves (the guard above). Measured on the head's built package: `object.zod.ts` is `src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten comments also reach `dist`: - `9dac1ae01` appears in `dist/data/index.d.ts` (the `filter-logic-conformance.ts` docblock) and in 4 bundled `.js` files; - `fc9ba76a5`, `f11fc61c5` and `b37231883` each appear in 22 bundled `.js` files, and `c459da6bc` in 12; - the positive control, the pre-existing `object.zod.ts` sentence 「Fail-CLOSED at both points」, appears in 11 bundled `.js` files. ## Gates (head `53c9070dfd`) - **Citation judging pass, run as CI runs it:** `pnpm check:issue-citations && node scripts/check-issue-citations.mjs` exits 0. The self-test passes 73 cases in 7 batteries. The live run judged 6 citations across 3 files: 3 resolve (`objectstack-ai#9138` twice, `objectstack-ai#11410`) and 3 resolve as a pull request (`objectstack-ai#13529`, the link). - **Doc authoring:** `pnpm check:doc-authoring` exits 0. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at the head derived 79 families, and all 79 exit 0. `--ran` reports 79 run, 0 NOT MEASURED, 0 unrun, and exits 0. A full `turbo run build` of `./packages/*` ran first, under the shared verify lock: 71 of 71 tasks, VERDICT command-exit 0. So no gate met an unbuilt prerequisite. - `pnpm --filter @objectstack/spec run check:generated`: under the lock against that build, `All 15 generated artifacts are up to date`, VERDICT command-exit 0. - **Tests and typecheck:** - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/data` under the lock: Test Files 107 passed (107), Tests 3527 passed, 1 todo (3528), VERDICT command-exit 0. It covers every test in `data/`, among them `object.test.ts`, which reads these schemas. - The 13 spec suites outside `src/data` that read the touched files' source text or pin their line numbers, under the lock: Test Files 13 passed (13), Tests 544 passed (544). They are stage 3's 12 (`scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`, `src/api/api-entry-graph.pin.test.ts`, `src/contracts/scoped-context.test.ts`, `src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`, `src/system/constants/platform-object-names.test.ts`, `src/type-alias-convention.pin.test.ts`, `src/ui/dashboard.test.ts`) plus `src/shared/union-author-message-pins.test.ts`, which pins `data/object.zod.ts:855`. - `pnpm --filter @objectstack/spec typecheck` under the lock exits 0, including `check:test-typecheck` (53 files, 251 errors, 138 pinned signatures held). - **Lint, as a proven narrowing at the head:** `eslint --no-inline-config --format json` over the 3 touched `.ts` files gives 3 files, 0 errors and 0 warnings. All 3 are in eslint's own population (`isPathIgnored` is false for each). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, which its own line 328 states), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. ## Acceptance notes - **Base.** The branch forked from `03b19d9cfd`, stage 3's landing. `origin/main` then moved two commits (`05077d4c26`, PR objectstack-ai#20532, and `288611e3e5`, PR objectstack-ai#20536), and neither touches `data/`. `dispatch-gates` flagged its derivation as stale because `scripts/regen-artifacts.mjs` had moved, so `origin/main` was merged in (`53c9070dfd`, a clean merge with no driver-deferred path) before the gates ran. The PR's delta against `origin/main` is exactly its 4 files. `origin/main` has since moved two more commits: `7e36a3cd7c` (PR objectstack-ai#20531) and `ba5927f714` (PR objectstack-ai#20460). Neither touches `data/` or anything the gate derivation reads, and a re-derivation prints the same 79 commands. A no-driver `merge-tree` of the head onto `ba5927f714`, from a bare shared clone, exits 0. So there is no second merge. - **Open PRs, re-read at 2026-09-29T02:01Z:** 9 open PRs, and none touches any of the six files. The `data/` files open PRs touch are objectstack-ai#20458's `analytics*` files, objectstack-ai#20504's `driver/turso.*`, and objectstack-ai#20545's `filter-number-comparand-declared-type.*`, which is disjoint. Since the claim, PR objectstack-ai#20460 has landed (`ba5927f714`) without touching `filter-subtree-provenance.ts`. That file's 3 dead sites are outside this claim's fence, so they are left for a later stage. - **The rung.** Two anchored changes also have ADR-0087 entries in `packages/spec/src/migrations`: `cbp-master-detail-required-forced` for objectstack-ai#8772, and `form-view-option-default-retired` for objectstack-ai#12868. The second entry's own header names commit `c459da6bc`. This PR takes the commit rung, as stages 1 to 3 did. The D3 id is the more durable in-repo record, if the ruling's first rung is later read to include those entries. - **What stays in `data/` after this stage: 58 dead sites.** - **12 comment sites in files other open work still holds.** `analytics.zod.ts`, `analytics-strictness-batchd.test.ts` and `analytics-date-range-two-bound-window.test.ts` hold 5 (objectstack-ai#20300, PR objectstack-ai#20458). `driver/turso.zod.ts` and `driver/turso.test.ts` hold 4 (objectstack-ai#20437, PR objectstack-ai#20504). `filter-subtree-provenance.ts` holds 3. It was held by objectstack-ai#20367 and is now free (see above). - **3 comment sites stage 3 left on purpose.** They are the test-read `[objectstack-ai#6259]` marker at `api-derivation.ts:163`, the test comment at `api-derivation.test.ts:232` that names it, and `field.zod.ts:370`, whose `objectstack-ai#6111` is objectui's number. - **43 string sites**, left as tokens: 41 test strings (2 of them in the held analytics and turso test files) and the 2 exported `AGGREGATION_CASES` note strings in `aggregation-conformance.ts` (`:398`, `:407`, objectstack-ai#11065), which objectstack-ai#20489's claim holds. - **Outside `data/`,** the card's other remaining items are unchanged: the migrations and ui areas, the `liveness/**` notes, the `why` strings, the `PROVENANCE_WAIVERS` reason, and `rest-server.zod.ts`. - **The citation gate's reach.** It defers `packages/**/*.test.ts`. No test file is touched here, so all 3 touched files are in its judging population. --- _Generated by [Claude Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… decision in words instead of a tracker number (stage 15) (objectstack-ai#21810) Part of objectstack-ai#20749 Clause-②: no Stage 15 of this card: the next area of class (e), the test strings shipped under `packages/spec/src`, as ruled in `5902360492` on objectstack-ai#20513. This stage takes the first name-ordered file group directly under `packages/spec/src/data/`: the 20 test files from `aggregate-field-type-compatibility.test.ts` to `date-range-presets.test.ts`. They carried 94 messages and 102 tracker ids, citing 60 records. Every one of those ids now either states what its record decided, in words (form D), or is dropped where the title already says it. Text only: no assertion, identifier, test count or code comment changes. ## Census at the base (`0a3480311a`) Instruments: `census10.cjs` (md5 `9d08602ab972b4b8643c90d64d40fa41`), `census.cjs` (md5 `6e42a45a926d375013c32d62f16a296e`), `census-wide.cjs` (md5 `c98410a19529c439adb0afbfb00026a2`) and `dirtable.cjs` (md5 `dda605c54745b4a60cc14c9a686e4eff`). They are byte-identical to the copies stages 10 to 14 used. A literal counts as a test title when its folded message is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` / `.only` chains included. Everything else is an "other" string. Both instruments read **1325 messages / 1406 ids in 282 files**, the seat's reading at `0a3480311a` (stage 14's head). | directory | files | messages / ids | titles | other | |:--|--:|--:|--:|--:| | `data/` (this PR: the first 20 files) | 95 | 468 / 501 | 445 / 475 | 23 / 26 | | `ui/` | 81 | 393 / 416 | 375 / 398 | 18 / 18 | | `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 | | `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 | | (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 | | `ai/` | 1 | 2 / 2 | 0 | 2 / 2 | | `contracts/` | 1 | 1 / 1 | 0 | 1 / 1 | | **total** | **282** | **1325 / 1406** | **1246 / 1323** | **79 / 83** | The group reads **94 messages / 102 ids in 20 files**, the seat's figures, file for file: | file (under `data/`) | messages / ids | titles | other | |:--|--:|--:|--:| | `aggregate-field-type-compatibility.test.ts` | 4 / 4 | 4 / 4 | 0 | | `analytics-date-range-closed-vocabulary.test.ts` | 1 / 1 | 1 / 1 | 0 | | `analytics-date-range-two-bound-window.test.ts` | 3 / 3 | 3 / 3 | 0 | | `analytics-query-window-integer.test.ts` | 2 / 2 | 2 / 2 | 0 | | `analytics-strictness-batchd.test.ts` | 9 / 9 | 9 / 9 | 0 | | `analytics.test.ts` | 6 / 6 | 6 / 6 | 0 | | `api-derivation.test.ts` | 6 / 6 | 6 / 6 | 0 | | `api-methods-batch-conformance.test.ts` | 3 / 4 | 1 / 1 | 2 / 3 | | `authoring-key-lint.test.ts` | 2 / 2 | 2 / 2 | 0 | | `autonumber-format.test.ts` | 3 / 3 | 3 / 3 | 0 | | `autonumber-unanchored-boundary.test.ts` | 3 / 4 | 3 / 4 | 0 | | `bulk-write-hook-conformance.test.ts` | 2 / 2 | 2 / 2 | 0 | | `calendar-day.test.ts` | 2 / 2 | 2 / 2 | 0 | | `context-tokens.test.ts` | 1 / 1 | 1 / 1 | 0 | | `currency-mode-family-closure.pin.test.ts` | 2 / 2 | 2 / 2 | 0 | | `currency-precision-iso4217.test.ts` | 7 / 7 | 7 / 7 | 0 | | `data-engine.test.ts` | 20 / 25 | 20 / 25 | 0 | | `datasource-credential-redaction.test.ts` | 6 / 6 | 6 / 6 | 0 | | `datasource.test.ts` | 10 / 10 | 10 / 10 | 0 | | `date-range-presets.test.ts` | 2 / 3 | 2 / 3 | 0 | | **20 files** | **94 / 102** | **92 / 99** | **2 / 3** | - **Controls.** Lit, a title: `data/document.test.ts` reads 2 / 2 at the head. Lit, "other" strings: the two in `data/external-lookup-retirement.test.ts` (`:89`, `:130`) still read at the head. Dark: the file comment at `data/analytics-strictness-batchd.test.ts:4` (it names the strictness batch by its number) reads 0. Planted in a scratch copy of the head `data/calendar-day.test.ts`: an id put into a title reads 1 / 1, and an id put into a comment reads 0. - **A wider pattern** (any `#` plus digits) reads the same totals in 19 of the 20 files. In `aggregate-field-type-compatibility.test.ts` it reads one more, a decision-batch number at `:150` that sits beside a cited record in the same literal. The gate's pattern needs three to five digits, so it is not counted there. - **At the head:** 1231 messages / 1304 ids in 262 files. The 20 files read 0 / 0 on both patterns, and no other file moved. ## How the area was chosen `data/` has no subdirectory to split by (449 ids directly under it, `data/driver/` 52), so its stages take name-ordered file groups near the ~100-id bound, as stage 14's report proposed. This census reads the first group at exactly 102, the claim's figure, so the rule needed no re-cut. **Named for the next stages** (re-cut from the head census, 1231 / 1304; `data/` 374 / 399 left): - `data/` in four more stages, name-ordered: 1. `default-value-shape.test.ts` to `filter-comparand-shape.test.ts`: 20 files, 94 messages / 100 ids; 2. `filter-comparand-type.test.ts` to `filter-view-operator-parity.test.ts`: 20 files, 95 / 99; 3. `filter.test.ts` to `object.test.ts`: 17 files, 103 / 114. `object.test.ts` alone carries 42, so no cut lands nearer the bound; 4. `query-transport.test.ts` to `validation.test.ts` (11 files, 34 / 34) with `data/driver/` (7 files, 48 / 52): 86 ids. - `ui/` 416, about four stages. `api/` 201, two. `system/` 165, two. The files directly in `src/`, 120, one. - The three docblock needles (`ai/build-progress.test.ts:236`, `:237`, `contracts/approval-service.test.ts:274`), one stage with their docblocks. ## What each id became 24 literals (28 ids) now state a decision in words. 2 literals (2 ids) get their subject back in words where the number stood in for it. 69 literals (72 ids) drop a number the title already explains. (95 literals in 94 messages: the `sys_organization` reason string is one message over two lines.) Every cited record was read with its comments through REST: 55 answer 200. objectstack-ai#6345, objectstack-ai#8876, objectstack-ai#9040, objectstack-ai#10194 and objectstack-ai#17014 answer 404, and their decisions were read from what landed: `e2798fa` (one driver vocabulary for start and migrate), `d634e66` (the username half of the URL userinfo grammar), `2420641` (a credential in the mongo `options` passthrough is refused), `2306a76` (`theme` / `analytics_cube` validated at the `/meta` write door) and `80aef80` (a one-day window for the one-day presets), each with its CHANGELOG entry. No cross-repo record is cited in this group. | record(s) | literal (under `data/`) | now reads | |:--|:--|:--| | objectstack-ai#11152 | `aggregate-field-type-compatibility.test.ts:150` | "accepts `sum` / `avg` / `min` / `max` over booleans — numbers on every backend, a ruling that outranks the refused-by-default rule". The maintainer ruled that booleans aggregate as numbers on every backend; decision batch 80 held that ruling over batch 59's blanket refusal of unnamed pairs. That batch number went with the id. | | objectstack-ai#4001 (3) | `analytics-strictness-batchd.test.ts:83`, `:248`, `:306` | "batch D, unknown keys refused — …" before "the doors the cube family is reachable through", "alias claims are true of the surfaces they point at" and "deliberate non-closures (re-verdicts, not omissions)". The campaign's decision: an unknown key is refused, not stripped. | | objectstack-ai#3878 (2) | `analytics-strictness-batchd.test.ts:270`, `:297` | "matching the dispatcher's bespoke hint at the /analytics entry" and "the retired-envelope tombstones still fire". The body is the bare `AnalyticsQuery`; the `{ cube, query }` envelope was retired with tombstones, and the entry answers 400 with a hint at `where`. | | objectstack-ai#18612 | `analytics.test.ts:314` | "a persisted cube heals at the door — the retired join `sql` / `relationship` are stripped (ADR-0087 D2)". | | objectstack-ai#3391 | `api-derivation.test.ts:16` | "api-derivation — one table resolves the effective operations from six primitives". The server is the only adjudicator, through one derivation table. | | objectstack-ai#3543 | `api-derivation.test.ts:286` | "vocabulary split — authors write six primitives, the wire speaks operations". The authored enum shrank; the wire vocabulary stayed byte-stable. | | objectstack-ai#15873 | `api-methods-batch-conformance.test.ts:221` | A declared reason string: "(a ruling grants `update`; both are column-clamped per row by ADR-0092 D2)". Option (a), decision batch 64. | | objectstack-ai#3786 | `authoring-key-lint.test.ts:37` | "lintAuthoredRecordKeys — an unknown authoring key is reported, not swallowed", the decision its source docblock records. | | objectstack-ai#6555 | `autonumber-format.test.ts:23` | "DEFAULT_AUTONUMBER_FORMAT / resolveAutonumberFormat — one declared default both sides read". Route 3: `{0000}` became the contract default, and both fallbacks went away. | | objectstack-ai#5038 | `bulk-write-hook-conformance.test.ts:114` | "records the after half as DELIVERED — the engine fires it once per row". | | objectstack-ai#5574 | `bulk-write-hook-conformance.test.ts:119` | "records the before half as DELIVERED — the engine dispatches it per row too". | | objectstack-ai#20126 | `currency-mode-family-closure.pin.test.ts:348` | "currency-mode family — the enumerating closure pin: `defaultCurrency` holds only under `fixed`". | | objectstack-ai#19992 | `currency-precision-iso4217.test.ts:163` | "the removed `currencyConfig.precision` at rest: a stored row carrying the baked `precision: 2` is served canonical". | | objectstack-ai#7918 | `currency-precision-iso4217.test.ts:224` | "… where the ISO 4217 width check used to refuse it". That check was the record's option A, later reversed. | | objectstack-ai#3407, objectstack-ai#6437 | `data-engine.test.ts:1185` | "DroppedFieldsEventSchema.reason — why a write dropped submitted fields, widened past the readonly pair". | | objectstack-ai#6262, objectstack-ai#6433, objectstack-ai#6435 | `data-engine.test.ts:1198` | "primary_key is the value the engine reports when it strips a payload id it ruled is not an identifier", the schema's own wording of that strip on the bulk and the by-id paths. | | objectstack-ai#8300 | `datasource-credential-redaction.test.ts:70` | "(the drift guard on the one credential-key definition)". | | objectstack-ai#8876 | `datasource-credential-redaction.test.ts:232` | "— the username half of the same alignment". | | objectstack-ai#8337 | `datasource-credential-redaction.test.ts:243` | "redactUrlCredentialQueryParams — the read half: a credential query parameter is never served back". | | objectstack-ai#8153 | `datasource.test.ts:673` | "— unchanged by the managed-row credentialsRef allowance". The ruling allowed `external.credentialsRef`, and only it, on managed rows. | | objectstack-ai#4614, objectstack-ai#8793 | `date-range-presets.test.ts:14` | "date-range preset vocabulary — one source of truth, read by both the UI and the data side". | **Subject restored (2 ids):** objectstack-ai#20126 at `currency-mode-family-closure.pin.test.ts:403` ("currency-mode closure controls — each rule can fail, and passes what it must") and objectstack-ai#7918 at `currency-precision-iso4217.test.ts:311` ("carries the measured anchors — 0 digits for JPY, 2 for USD, 3 for KWD"). That literal moved from double to single quotes, since it no longer holds an apostrophe. **Dropped only (72 ids):** objectstack-ai#1603, objectstack-ai#2377, objectstack-ai#3026, objectstack-ai#3391, objectstack-ai#3543, objectstack-ai#3545, objectstack-ai#3795 (9), objectstack-ai#4001 (2), objectstack-ai#4286, objectstack-ai#4346 (2), objectstack-ai#4538, objectstack-ai#4583, objectstack-ai#5586, objectstack-ai#6345, objectstack-ai#6555, objectstack-ai#6560, objectstack-ai#7178 (5), objectstack-ai#7265, objectstack-ai#7287 (2), objectstack-ai#7802 (2), objectstack-ai#8032, objectstack-ai#8057 (2), objectstack-ai#8153 (7), objectstack-ai#8336, objectstack-ai#8337, objectstack-ai#9040, objectstack-ai#10194, objectstack-ai#10414, objectstack-ai#13802, objectstack-ai#16041, objectstack-ai#16632, objectstack-ai#17014, objectstack-ai#17296, objectstack-ai#17598 (2), objectstack-ai#18278, objectstack-ai#19992 (3), objectstack-ai#20011, objectstack-ai#20300 (2), objectstack-ai#20550, objectstack-ai#20600, objectstack-ai#20808 (3), objectstack-ai#21365 (2). - Each of these titles already states the decision it pins: for example "empty array → deny-all (flipped semantics)" for objectstack-ai#3391, "accepts the BARE query string — the canonical ADR-0061 D1 spelling" for objectstack-ai#7178, or "`currencyConfig.precision` is removed: refused with the prescription, whatever its value" for objectstack-ai#19992. - **Small rewordings that carry no new claim:** `analytics-strictness-batchd.test.ts:307` reads "are CLOSED now" where it named the record; `autonumber-unanchored-boundary.test.ts:51` reads "(ruled: mixed content is out of contract)"; `datasource.test.ts:553` reads "(the happy path)". The circled part numbers after objectstack-ai#17598 went with the id. - **The two `api-methods-batch-conformance.test.ts` reason strings** (`sys_api_key`, `sys_organization`) end "rather than hitting /batch." now. The table is read only through `!== undefined`, so no assertion reads their text. ## Readers - **Test-name filters:** none. A tracked-tree search for `-t` and `--testNamePattern` finds only `packages/qa/dogfood/README.md:142` (`-t "owner-scoped"`), which is unrelated. - **Snapshots:** none. No `__snapshots__` directory exists under `data/`, and no `.snap` file is tracked under `packages/spec`. - **Projects:** two touched files are listed in `packages/spec/vitest.repo-tests.json`: `api-methods-batch-conformance.test.ts` and `currency-mode-family-closure.pin.test.ts`. Both were run in the `repo` project at the base and at the head, and the other 18 in `local`. - **By substring:** every old literal, plus a window around each id (289 needles), was searched across the tracked tree outside its own file. No gate, doc, filter, snapshot or `scripts/check-*.mjs` self-test reads one. The 14 hits are: - **sibling titles in other lanes:** `service-analytics` `aggregate-nontemporal-measure-refusal.test.ts:344` and `objectql` `engine-autonumber-default-format.test.ts:248`; - **this card's later `data/` stage:** `data/driver/postgres.test.ts:169`, the same "placeholders are not resolved here" title, already in the census; - **comments, CHANGELOG, an audit ledger and liveness evidence:** `lint` `validate-dataset-measure-aggregates.test.ts:179`, `service-analytics` `dataset-compiler.ts:227`, `objectql` `engine.ts:6206` and `:6272`, `analytics.zod.ts:1002`, `docs/audits/2026-07-unknown-key-strictness-ledger.md:728`, two `packages/spec/CHANGELOG.md` entries and the `liveness/field.json:218` evidence string, which quotes the `engine.ts` comment. None reads a test title. - **Same-text titles named in stage 14's ACCEPT** (`(objectstack-ai#15680)`, `(objectstack-ai#5955)`, `objectstack-ai#3896 close-out`): none falls in this group. ## Text-only proof Stage 10's scratch tool (`textonly10.cjs`, md5 `d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file on three legs: 1. **Skeleton:** the full AST, with string pieces masked. It must be identical. 2. **Comments:** every comment, byte-equal. 3. **Strings:** each changed string leaf must sit in a test-call title position or on a declared line, must carry a tracker id before, and must carry no `#` plus digits after. The declared lines are the three reason-string leaves in `api-methods-batch-conformance.test.ts`. - **Result:** 20 of 20 files SAME on all three legs, as predicted in writing before the run. - **Totals:** 95 changed literals, 92 titles and 3 declared. The diff's `+` and `-` lines are exactly the 95 planned lines, and every file keeps its line count. - **Controls (10 of 10 as predicted, on scratch copies, each anchor hit once):** identifier rename DIFF; numeric literal DIFF; comment edit COMMENT DIFF; a non-title string given an id VIOLATION; a rewritten title given a new id VIOLATION; a title that was id-free at base edited VIOLATION; one title reverted to base SAME; a declared string given a new id VIOLATION; an undeclared `expect` message changed VIOLATION; a title re-split into a `+` chain DIFF. **Test counts:** the 20 files were run at the base, in a separate base worktree, and at the head, with `--project local --project repo`. Both sides read 553 / 553 passed, with the same count and status sequence per file in 20 of 20. 291 full test names change, and each equals the base name with the planned replacements applied (0 mismatches). No full name repeats on either side. ## `main` merged in, once objectstack-ai#21800 (the console pin bump) landed while this branch was being verified, and it rewrites the comment block at `:61-77` of `api-methods-batch-conformance.test.ts`. This PR edits only string literals in that file, more than 100 lines below the block, so `origin/main` (`18c2ddc1ec`, which also carries objectstack-ai#21801) was merged in with a plain merge, no rebase, and no conflict. The PR's delta against `main` is still exactly the 20 files, +95 / -95. Every reading in this body was re-taken on the merged head `bf16ad1190`, against `18c2ddc1ec` as the base: the census (1325 / 1406 there, 1231 / 1304 here, unchanged by the two commits), the text-only proof and its controls (the three declared lines now sit at `:202`, `:230` and `:235`), the 20-file runs, the full build, the suite, the typecheck and the gates. Re-fetched just before this PR opened, `origin/main` was one commit further (`75ddcd1b41`, objectstack-ai#21805, in `cloud-connection`, `metadata-core` and `runtime`). It touches no `packages/spec` path and no file here, so it was not merged. ## Changeset: `skip-changeset` Measured, not assumed: - `npm pack --dry-run` of `@objectstack/spec` lists 2068 files. 0 of the 20 touched files are in it, and no `*.test.ts` at all. Of `src/`, only the `*.zod.ts` sources ship: the controls `src/data/analytics.zod.ts`, `src/data/data-engine.zod.ts` and `dist/data/index.js` are in it. - In the built `dist/`, five new phrases and four old literals each read in 0 files. The control `Unrecognized key(s) on` reads in 42. So this PR publishes nothing, and no changeset is added. ## Verification (at `bf16ad1190`) - `pnpm turbo run build` over all packages: 71 / 71 (also 71 / 71 at the pre-merge head `89c4b300c2`). - `@objectstack/spec`: - `vitest run --project local`: 615 files, 18360 passed, 1 todo. - `typecheck` exit 0, including `check:test-typecheck` (52 files / 246 errors / 135 pinned signatures held). Its program holds all 20 touched files, counted with `tsc --listFilesOnly -p tsconfig.test.json`. - `check:generated`: all 15 generated artifacts up to date after the merge. - **Gates:** `dispatch-gates --commands` derived 79 families, the same set as stages 13 and 14, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN. - The five roster families whose rosters sit under a touched directory were also run, and each exits 0: `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **ESLint, a proven narrowing:** `--no-inline-config` over the 20 files reads 0 errors and 0 warnings. The population comes from ESLint's own config: 20 configured, 0 ignored. No file sets `parserOptions.project` or `projectService`, so no untouched file's verdict can move. - `check-governed-merges --test`: NOT governed, 190 changed lines. ## Acceptance notes - **No needle in this group.** Every id was a title or a declared reason string; no expected value of an assertion over a source docblock was found. The three known needles are untouched. - **Same-id test titles in other packages** are their lanes' test-string shares. A search of `describe` / `it` / `test` lines outside `packages/spec` finds 156 lines citing ids this PR handled, in 79 files of 23 packages: `objectql` 73 (29 files), `rest` 18 (7), `runtime` 7 (4), `plugin-security` 6 (5), `cli` 6 (3), `lint` 6 (4), `service-datasource` 6 (3), `driver-sql` 4 (4), `platform-objects` 4 (2), `plugin-approvals` 3 (2), `service-automation` 3 (2), `driver-mongodb` 3 (1), `plugin-auth` 3 (1), `service-analytics` 3 (3), `metadata-core` 2 (1), `plugin-hono-server` 2 (1), and one each in `client`, `triggers`, `core`, `metadata-protocol`, `qa/dogfood`, `types` and `driver-memory`. - **Two spec test files outside `src/`** carry same-id titles: `packages/spec/scripts/file-description.test.ts:66` and `packages/spec/scripts/format-type.test.ts:85`. They are outside class (e) as ruled ("the test strings shipped under `src/`"). - **Numeric delivery fields:** `bulk-write-hook-conformance.test.ts:115-116` and `:129-130` assert `engineDeliveryIssue: 5038` / `5574`, numbers in the source contract table. They are not strings, the gate's pattern cannot see them, and they are not this card's share. - **Code comments still carry ids** in these files and their sources, for example the header of `analytics-strictness-batchd.test.ts` and the `SINGLE_RECORD_WRITE_ONLY` comments in `api-methods-batch-conformance.test.ts`. Comments are not this card's share, and none is touched here. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #6262
按分诊评论「Scope as queued = route A only」执行:只做 A 案(派发层剥离),零 verdict 变更,B 案(响亮拒绝)不在本次范围。
问题
update(o, { id: { $in: ['a','b'] }, title: 'x' }, { multi: true })的派发自 #5748 裁 A / PR #5919 起就是对的:算子对象不是主键,不再遮蔽派发阶梯,声明的 bulk intent 照做,调用落到driver.updateMany。#5919 没做、#5922 也按 PD #10 明确留在范围外的,是载荷那一半。实测复现(worktree @
origin/main,记录型 driver 驱动真实引擎,新增测试在打补丁前的失败断言原文):与 issue 正文的 PROBE 逐字一致:驱动被要求把一个序列化的算子对象写进每一条命中行的主键列。五个后端会对这件事各给一个答案(#5240 / #4434 家族),而在接受它的后端上,命中行的身份不可逆地丢失。
修法
packages/objectql/src/engine.ts的 update multi 分支载荷组装点(分支第一件事,encryptSecretFields之前):载荷带id键时剥掉,并按warn记一条点明后果与两种正确写法的日志。论证只有一句:走到 multi 分支本身就意味着
resolveEngineUpdateDispatch答了multi,即它在两个 id 来源里都没找到真值标量 id —— 所以此刻data.id里的任何东西(算子对象、数组、null、假值标量)都是引擎已经裁定不是主键的值。剥离是同一个问题的同一个答案多用在一层上,不是第二个答案:不是主键的东西,也就不该坐在主键列上。必答项一:与 #5922 / #5748 的语义一致性
与 #5748(裁 A / PR #5919)一致 —— 这是它的另一半,不是它的回退。
#5748 把
data.id送进了和where.id同一个标量测试,答案是「这个data.id不是主键」;它据此做了一件事(不再遮蔽阶梯,multi照做)。本 PR 据同一个答案做第二件事(不再留在主键列位置)。ENGINE_UPDATE_DISPATCH_CASES一行未动,operator object in data.id WITH multi:true仍是'multi',engine-update-dispatch.test.ts用真实引擎逐条驱动的 25 例全绿(该文件 36/36)。反过来的 B 案要反转这条刚落地的 case,那才是对裁 A 的部分回退。与 #5922 一致 —— 它留下的正是这条轴,而不是校验轴。
#5922 收口的是「声明值为标量的字段」上的算子对象,走
record-validator;id被SKIP_FIELDS按设计跳过(引擎自有列),因为这一格的裁定写在派发层。若改在 record-validator 里拒收同一个调用,就是对同一个问题给出第二个答案 —— 正是engine-update-dispatch.ts这一族模块被抽出来防止的事(#4550 / #4434)。本 PR 落在派发已给出的答案上,record-validator.ts一字未动,两条轴仍各管各的。「一个问题一个答案」不被破坏的可检验形式:唯一的判定仍只有
resolveEngineUpdateDispatch一处;剥离不重新问「这是不是 id」,而是消费分支本身携带的答案(kind === 'multi'⇒ 无 id)。代码里没有第二个标量测试、没有??兜底、没有手抄的if。必答项二:B 案(响亮拒绝)将来若裁定,要动哪里(只答不做)
packages/metadata-core/src/engine-update-dispatch.ts—— 判定本体。resolveEngineUpdateDispatch需要新增一个reject前置:data.id存在且非标量真值 且options.multi为真 ⇒reject(今天这条落在if (options?.multi) return { kind: 'multi' })。同文件的ENGINE_UPDATE_DISPATCH_CASES里operator object in data.id WITH multi:true与array data.id with multi:true两例的expect由'multi'改为'reject',模块头 point 2 的叙述需要重写(「不再遮蔽阶梯」变成「非标量data.id本身即拒绝理由」)。新拒绝语句大概率需要一条独立的 message 常量,而不是复用ENGINE_UPDATE_REJECT_MESSAGE(「既没点名一行也没声明 bulk」和「声明了 bulk 但载荷里塞了个非 id 的 id」是两种不同的作者错误,共用一句话会把诊断打回原点)。packages/objectql/src/engine.ts—— 生产者侧。update()末尾那条else { throw new Error(ENGINE_UPDATE_REJECT_MESSAGE) }是 hook 改写后重问判定的地方,需要按新 message 分叉;本 PR 加的剥离块整块删除(拒绝之后没有载荷可剥)。assertEngineUpdateDispatch上的假引擎自动跟进(这正是该模块存在的理由,不需要逐个改);但scripts/check-engine-double-contract.mjs的 DEBT 账本里那 133 条未钉的替身会开始与生产者分歧,需要重新测量。update_record把新拒绝映射成 4xx 而非 500 —— 属packages/rest的mapDataError与 automation 侧执行器,拒绝语义落地时才有意义。data.id不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式options.multi: true#5748 裁 A 的部分回退,而 A 与 B 在同一个业务场景上给用户不同的东西 —— A 让「声明了 bulk intent 就照做」继续成立(作者多写了个id谓词,行集由where决定),B 认为这种调用形状本身即作者错误、必须响亮。这是产品判断,不是实现判断。变更清单
packages/objectql/src/engine.tsid剥离 + 论证注释(+51)packages/objectql/src/engine-update-multi-payload-id.test.ts.changeset/engine-update-multi-payload-id-strip.md@objectstack/objectqlpatch⛔ 未触碰:
ENGINE_UPDATE_DISPATCH_CASES/ metadata-core 全包、record-validator.ts、非 multi 路径、事务区(#6403)、自增、剥离时序区(#5591 / #6343)、summary、content/docs/releases/。测试
新增
packages/objectql/src/engine-update-multi-payload-id.test.ts,11 例三组:null的data.id+multi⇒updateMany载荷无id键,title照常落地;调用方传入的载荷对象不被就地改写(剥离走浅拷贝,与本路径其它 strip 一致)。where.id侧不变:multi且载荷从未带 id ⇒ 载荷与行域 AST 双双原样;where: { id: { $in: [...] } }仍由 AST 选行,载荷不动。{ id: 0 }/{ id: '' }+multi的判定仍是multi(engine-delete-dispatch 的共享判定与 ObjectQL.delete 在「假值标量 id」上不一致 ——where: { id: 0 }判定答 by-id,引擎却 reject #5747 / ObjectQL.update 的data.id不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式options.multi: true#5748 语义,原样);单 id 路径(data.id标量压过multi、where.id标量、以及 ObjectQL.update 的data.id不做标量测试 —— 载荷里的算子对象被当成主键绑定,且盖过显式options.multi: true#5748 的「算子data.id旁有标量where.id」头号形状)全部driver.update,载荷按原样送达 —— 主键走独立参数,载荷里的id是冗余而非破坏,本 PR 不动它,并按现状钉死,使将来任何扩大剥离范围的动作都必须是刻意的。反向验证(方向:红,如预测)
肢 A = 去掉剥离。这次的测量顺序天然就是这个实验:测试文件先在未打补丁的
origin/main上跑,engine.ts一字未改 —— 5 例红,且失败信息直接印出问题载荷:打补丁后同一文件 11/11 绿。注意
does not mutate the payload object the CALLER handed in一例在补丁前就是绿的(引擎当时根本不剥,自然不会改到调用方对象)—— 它是对修法的护栏,不是复现用例,如实记在此处而非充作反向证据。同一次运行还证明了「只有这 5 例动了」:补丁前整包
5 failed | 2327 passed (2332),补丁后2332 passed (2332),总数一致。命令与实测输出
消费半径(multi 分支的下游调用者)另跑:
合并
origin/main(至7618ee814)后按 AGENTS.md §10 重跑:packages/spec在对侧动过,故pnpm --filter @objectstack/spec build && check:generated→All 10 generated artifacts are up to date;objectql全量 test + typecheck 复跑仍全绿。Generated by Claude Code